using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections.Generic;
using Microsoft.Isam.Esent.Interop;
using System.Runtime.Serialization;

namespace Microsoft.Isam.Esent.Serialization
{
	// ACHTUNG! Untested code.
	// This class implements binary imported and exporter for a ESE database.
	// The main goal is table-granular backup & restore of arbitrary data.
	// The long-valued columns are handled in a way that allows values to be long without affecting performance.
	class ImportExportBinary : ImportExport
	{
		readonly BinaryFormatter bf = new BinaryFormatter();

		void CopyStream( Stream sFrom, Stream sTo, Int64 cbSize )
		{
			while( cbSize > 0 )
			{
				int nRead = sFrom.Read( buff, 0, buff.Length );
				if( nRead <= 0 ) throw new SerializationException( "Unexpected end of stream" );
				sTo.Write( buff, 0, nRead );
				cbSize -= nRead;
			}
		}

		bool SaveVal( BinaryWriter bw, JET_COLUMNID idColumn, bool bLong, int itagSequence )
		{
			JET_RETINFO ri = new JET_RETINFO();
			ri.itagSequence = itagSequence;

			// Short value
			if( !bLong )
			{
				int cbSize;
				Api.JetRetrieveColumn( sess.idSession, idTable, idColumn,
					buff, buff.Length, out cbSize, RetrieveColumnGrbit.None, ri );
				bw.Write( cbSize );
				if( cbSize <= 0 ) return false;
				bw.Write( buff, 0, cbSize );
				return true;
			}

			// Long values
			ColumnStream cs = new ColumnStream( sess.idSession, idTable, idColumn );
			cs.Itag = itagSequence;
			Int64 cbTotal = cs.Length;
			bw.Write( cbTotal );
			if( cbTotal <= 0 ) return false;
			bw.Flush();
			CopyStream( cs, bw.BaseStream, cbTotal );
			return true;
		}

		bool LoadVal( BinaryReader br, JET_COLUMNID idColumn, bool bLong, int itagSequence )
		{
			JET_SETINFO si = new JET_SETINFO();
			si.itagSequence = 0;

			// Short value
			if( !bLong )
			{
				int cbSize = br.ReadInt32();
				if( cbSize <= 0 ) return false;
				br.Read( buff, 0, cbSize );
				Api.JetSetColumn( sess.idSession, idTable, idColumn,
					buff, cbSize, SetColumnGrbit.None, si );
				return true;
			}

			// Long values
			Int64 cbTotal = br.ReadInt64();
			if( cbTotal <= 0 ) return false;

			ColumnStream cs = new ColumnStream( sess.idSession, idTable, idColumn );
			cs.Itag = itagSequence;
			CopyStream( br.BaseStream, cs, cbTotal );
			return true;
		}

		int Export( BinaryWriter bw )
		{
			int cols = schema.Count;
			bool[] bMultiVals = new bool[ cols ];
			bool[] bLongVals = new bool[ cols ];

			// Export schema
			bw.Write( cols );
			for( int i=0; i < cols; i++ )
			{
				TypeSerializer.ColumnInfo ci = schema[ i ];
				bw.Write( ci.columnName );
				bw.Flush();
				bf.Serialize( bw.BaseStream, ci.attrib.getColumnKind() );
				var cd = ci.attrib.getColumnDef();
				bf.Serialize( bw.BaseStream, cd );
				bMultiVals[ i ] = 0 != ( cd.grbit & ColumndefGrbit.ColumnMultiValued );
				bLongVals[ i ] = ( cd.coltyp == JET_coltyp.LongBinary || cd.coltyp == JET_coltyp.LongText );
			}

			// Export data
			int nExported = ExportData
			(
				( int nRecord ) =>
				{
					bw.Write( nRecord );
					for( int i=0; i < cols; i++ )
					{
						if( bMultiVals[ i ] )
						{
							for( int j=1; true; j++ )
								if( !SaveVal( bw, schema[ i ].idColumn, bLongVals[ i ], j ) )
									break;
						}
						else
							SaveVal( bw, schema[ i ].idColumn, bLongVals[ i ], 1 );
					}
				}
			);
			bw.Write( (int)( -1 ) );
			return nExported;
		}

		int Import( BinaryReader br )
		{
			// validate the schema
			int cols = br.ReadInt32();
			if( schema.Count != cols ) throw new SerializationException( "Wrong columns count: stored " + cols + ", must be " + schema.Count + "." );

			bool[] bMultiVals = new bool[ cols ];
			bool[] bLongVals = new bool[ cols ];

			for( int i=0; i < cols; i++ )
			{
				TypeSerializer.ColumnInfo ci = schema[ i ];
				string strName = br.ReadString();
				if( strName != ci.columnName ) throw new SerializationException( "Wrong column name: stored '" + strName + "', must be '" + ci.columnName + "'." );

				eColumnKind ck = (eColumnKind)bf.Deserialize( br.BaseStream );
				if( ck != ci.attrib.getColumnKind() ) throw new SerializationException( "Wrong column kind: stored '" + ck.ToString() + "', must be '" + ci.attrib.getColumnKind().ToString() + "'." );

				JET_COLUMNDEF jcdStored = (JET_COLUMNDEF)bf.Deserialize( br.BaseStream );
				JET_COLUMNDEF jcd = ci.attrib.getColumnDef();

				if( jcd.coltyp != jcdStored.coltyp ) throw new SerializationException( "Mismatched type of the column '" + strName + "'" );
				if( jcd.cp != jcdStored.cp ) throw new SerializationException( "Mismatched codepage of the column '" + strName + "'" );
				if( jcd.grbit != jcdStored.grbit ) throw new SerializationException( "Mismatched bit flags of the column '" + strName + "'" );

				bMultiVals[ i ] = 0 != ( jcd.grbit & ColumndefGrbit.ColumnMultiValued );
				bLongVals[ i ] = ( jcd.coltyp == JET_coltyp.LongBinary || jcd.coltyp == JET_coltyp.LongText );
			}

			return ImportData
			(
				( int nRecord ) =>
				{
					int nCur = br.ReadInt32();
					if( nCur < 0 ) return false;
					if( nCur != nRecord ) throw new SerializationException( "The row data is corrupt." );
					return true;
				},
				( int nRecord ) =>
				{
					for( int i=0; i < cols; i++ )
					{
						if( bMultiVals[ i ] )
						{
							for( int j=1; true; j++ )
								if( !LoadVal( br, schema[ i ].idColumn, bLongVals[ i ], j ) )
									break;
						}
						else
							LoadVal( br, schema[ i ].idColumn, bLongVals[ i ], 0 );
					}
					return false;
				}
			);
		}

		public ImportExportBinary( EseCursorBase cur ) : base(cur) { }

		public ImportExportBinary( iSerializerSession _sess, JET_TABLEID _idTable, TypeSerializer _serializer )
			 : base( _sess, _idTable, _serializer ) { }

		public override int Export( Stream stm )
		{
			using( BinaryWriter bw = new BinaryWriter(stm) )
				return Export( bw );
		}

		public override int Import( Stream stm )
		{
			using( BinaryReader br = new BinaryReader( stm ) )
				return Import( br );
		}
	}
}